package com.avalon;

import java.io.Closeable;
import java.io.File;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.List;
import java.util.ArrayList;
import java.util.jar.JarFile;
import java.util.Scanner;

/**
 * 服务器的启动类
 */
public final class Boot {

	private static final Logger logger = Logger.getLogger(Boot.class.getName());

	private static volatile Process process = null;

	private Boot()
	{

	}

	/**
	 * 
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception
	{
		if (args.length > 1)
		{
			logger.log(Level.SEVERE, "Invalid number of arguments");
			throw new IllegalArgumentException("Invalid number of arguments");
		}

		// 从配置文件中读取配置
		SubstitutionProperties properties = null;
		if (args.length == 0)
		{
			properties = BootEnvironment.loadProperties(null);
		} else
		{
			properties = BootEnvironment.loadProperties(args[0]);
		}

		// get the java executable and verify version
		String javaHome = properties.getProperty(BootEnvironment.JAVA_HOME);
		if (System.getProperty("java.home").equals(javaHome) && System.getProperty("java.version").startsWith("1.5."))
		{
			logger.log(Level.SEVERE, "Project  Server requires Java 6 or higher");
			logger.log(Level.SEVERE, System.getProperty("java.home") + "&&" + System.getProperty("java.version"));
			throw new IllegalStateException("Project Server requires Java 6 or higher");
		}
		
		String javaCmd = javaHome + File.separator + "bin" + File.separator + "java";

		// 构建命令
		ExtJarGraph extGraph = new ExtJarGraph();
		List<String> executeCmd = new ArrayList<String>();
		executeCmd.add(javaCmd);
		executeCmd.add("-classpath");

		executeCmd.add(bootClassPath(properties, extGraph));

		String extPropertiesFile = extGraph.getPropertiesFile();
		if (extPropertiesFile != null)
		{
			executeCmd.add("-Dcom.zero.ava.ext.properties=" + extPropertiesFile);
		}

		// command-line properties for JMX management
		executeCmd.add("-Dcom.sun.management.jmxremote.port=" + properties.getProperty(BootEnvironment.JMX_PORT));
		if (!properties.getProperty(BootEnvironment.DISABLE_JMX_SECURITY).equals("false"))
		{
			executeCmd.add("-Dcom.sun.management.jmxremote.authenticate=false");
			executeCmd.add("-Dcom.sun.management.jmxremote.ssl=false");
		}

		for (String j : bootJavaOpts(properties))
		{
			executeCmd.add(j);
		}
		executeCmd.add(BootEnvironment.KERNEL_CLASS);
		executeCmd.add(properties.getProperty(BootEnvironment.BOOT_PROPERTIES));
		executeCmd.add(properties.getProperty(BootEnvironment.CONFIG_PROPERTIES));
		logger.log(Level.CONFIG, "Execute path = " + executeCmd);
		
		// build the process
		ProcessBuilder pb = new ProcessBuilder(executeCmd);
		pb.directory(new File(properties.getProperty(BootEnvironment.PROJECT_HOME)));
		pb.redirectErrorStream(true);

		// get the output stream
		OutputStream output = System.out;
		String logFile = properties.getProperty(BootEnvironment.LOG_OUTPUT);
		if (logFile != null)
		{
			try
			{
				// attempt to create any necessary parent directories
				// for the log file
				File log = new File(logFile);
				File parentDir = log.getParentFile();
				if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs())
				{
					logger.log(Level.SEVERE, "Unable to create log directory : " + parentDir);
					throw new IOException("Unable to create log directory : " + parentDir);
				}

				// create a stream for the log file
				output = new BufferedOutputStream(new FileOutputStream(logFile));
				logger.log(Level.INFO, "Redirecting log output to: " + logFile);
			} catch (FileNotFoundException e)
			{
				logger.log(Level.SEVERE, "Unable to open log file", e);
				throw e;
			}
		}

		// install a handler to cleanup when the process is killed directly
		Runtime.getRuntime().addShutdownHook(new Thread() {

			public void run()
			{
				if (process != null)
				{
					process.destroy();
					closeStream(process.getOutputStream());
					closeStream(process.getInputStream());
					closeStream(process.getErrorStream());
				}
			}

			private void closeStream(Closeable c)
			{
				try
				{
					c.close();
				} catch (IOException ioe)
				{
				}
			}
		});

		// run the process
		try
		{
			process = pb.start();
			new Thread(new StreamPipe(process.getInputStream(), output)).start();
			process.waitFor();
		} catch (IOException e)
		{
			logger.log(Level.SEVERE, "Unable to start process", e);
			throw e;
		} catch (InterruptedException i)
		{
			logger.log(Level.WARNING, "Thread interrupted", i);
		} finally
		{
			if (process != null)
			{
				process.destroy();
			}
			output.close();
		}
	}

	/**
	 * 构建启动时的CLassPath
	 * 
	 * @param env
	 *            环境配置文件
	 * @param extGraph
	 *            扩展
	 * @return
	 */
	private static String bootClassPath(Properties env, ExtJarGraph extGraph)
	{
		// 确认 PROJECT_HOME
		String avalonHome = env.getProperty(BootEnvironment.PROJECT_HOME);
		if (avalonHome == null)
		{
			return "";
		}

		// locate PROJECT_HOME directory
		File sgsHomeDir = new File(avalonHome);
		if (!sgsHomeDir.isDirectory())
		{
			return "";
		}
		StringBuilder buf = new StringBuilder();

		// add jars from PROJECT_HOME/lib
		File sgsLibDir = new File(avalonHome + File.separator + "lib");
		for (File sgsJar : sgsLibDir.listFiles())
		{
			if (sgsJar.isFile() && sgsJar.getName().endsWith(".jar"))
			{
				if (buf.length() != 0)
				{
					buf.append(File.pathSeparator + sgsJar.getAbsolutePath());
					logger.info("add Jar" + File.pathSeparator + sgsJar.getAbsolutePath());
				} else
				{
					buf.append(sgsJar.getAbsolutePath());
					logger.info("add Dir" + File.pathSeparator + sgsJar.getAbsolutePath());
				}
			}
		}
		String jarTool = env.getProperty("CLASS_JAVA_TOOL");
		if (jarTool != null)
		{
			File file=new File(jarTool);
			buf.append(File.pathSeparator + file.getAbsolutePath().trim());
		}

		// recursively add jars from DEPLOY
		File sgsDeployDir = new File(env.getProperty(BootEnvironment.DEPLOY));
		List<File> jars = new ArrayList<File>();
		appJars(sgsDeployDir, jars);
		for (File jar : jars)
		{
			if (buf.length() != 0)
			{
				buf.append(File.pathSeparator + jar.getAbsolutePath());
			} else
			{
				buf.append(jar.getAbsolutePath());
			}
		}

		// include the additional classpath if specified
		String addPath = env.getProperty(BootEnvironment.CUSTOM_CLASSPATH_ADD);
		if (addPath != null)
		{
			if (buf.length() != 0)
			{
				buf.append(File.pathSeparator + addPath);
			} else
			{
				buf.append(addPath);
			}
		}

		return buf.toString();
	}

	/**
	 * Splits the {@code $JAVA_OPTS} configuration property into a list of
	 * {@code String} objects consumable by a {@link ProcessBuilder}.
	 * <p>
	 * The split operation will break down the property specified by
	 * {@code $JAVA_OPTS} into tokens delimited by whitespace. Additionally,
	 * quoted strings that include whitespace will be treated as a single token.
	 * 
	 * @param env
	 *            the environment
	 * @return a list of {@code String} objects that represent the individual
	 *         components of the {@code JAVA_OPTS} configuration property
	 * @throws IllegalArgumentException
	 *             if the {@code JAVA_OPTS} configuration property has an
	 *             invalid format
	 */
	private static List<String> bootJavaOpts(Properties env)
	{
		String javaOpts = env.getProperty(BootEnvironment.JAVA_OPTS, "");

		Scanner s = new Scanner(javaOpts);
		List<String> realTokens = new ArrayList<String>();
		while (s.hasNext())
		{
			if (s.hasNext("\\\".*"))
			{
				String nextToken = s.findInLine("\\\".*?\\\"");
				if (nextToken == null)
				{
					throw new IllegalArgumentException("Invalid " + BootEnvironment.JAVA_OPTS + " format");
				} else
				{
					realTokens.add(nextToken.substring(1, nextToken.length() - 1));
				}
			} else
			{
				realTokens.add(s.next());
			}
		}

		return realTokens;
	}

	/**
	 * Helper method that recursively searches the given directory and adds any
	 * jar files found to the jars list.
	 * 
	 * @param directory
	 *            directory to search for jar files
	 * @param jars
	 *            list of Files to add any jar files found
	 * @return the number of jar files found that have a
	 *         {@link BootEnvironment.DEFAULT_APP_PROPERTIES} file in them
	 */
	private static void appJars(File directory, List<File> jars)
	{
		if (directory.isDirectory() && directory.canRead())
		{
			for (File f : directory.listFiles())
			{
				if (f.isFile() && f.getName().endsWith(".jar"))
				{
					try
					{
						JarFile jar = new JarFile(f);
						jars.add(f);
					} catch (IOException e)
					{
						logger.log(Level.WARNING, "File " + f.getAbsolutePath() + " is not a jar file");
					}
				} else if (f.isDirectory())
				{
					appJars(f, jars);
				}
			}
		}

	}

	/**
	 * Helper method that recursively searches the given directory for extension
	 * jar files, adding them to a list and separate graph of discovered
	 * extensions.
	 * 
	 * @param directory
	 *            the directory to search for jar files
	 * @param jarFileNames
	 *            list of file names for the discovered, valid jars
	 * @param graph
	 *            a collection of discovered extension jar files
	 */
	private static void extJars(File directory, List<String> jarFileNames, ExtJarGraph graph)
	{
		if (directory.isDirectory() && directory.canRead())
		{
			for (File f : directory.listFiles())
			{
				if (f.isFile() && f.getName().endsWith(".jar"))
				{
					try
					{
						graph.addJarFile(new JarFile(f));
						jarFileNames.add(f.getAbsolutePath());
					} catch (IOException e)
					{
						// not a jar file, log and ignore
						logger.log(Level.WARNING, "Extension file " + f.getAbsolutePath() + " is not a jar file");
					}
				} else if (f.isDirectory())
				{
					extJars(f, jarFileNames, graph);
				}
			}
		}
	}

}
